/*
 ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1

 * The contents of this file are subject to the Mozilla Public License Version 
 * 1.1 (the "License"); you may not use this file except in compliance with 
 * the License. You may obtain a copy of the License at 
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Blazing Games Inc. (Billy D. Spelchan) code.
 *
 * The Initial Developer of the Original Code is
 * Blazing Games Inc.
 * Portions created by the Initial Developer are Copyright (C) 1999, 2003, 2005-2006
 * the Initial Developer. All Rights Reserved.
 * 
 * Contributor(s):
 *
 ***** END LICENSE BLOCK *****
 */
package com.blazinggames.holeman;

import java.applet.AudioClip;
import java.io.*;
import java.net.*;
import java.util.Vector;
import java.util.Random;

public class Map {
// *** CONSTANTS ***
  public static final int SETLEVELS = 8;
  public static final int MAPWIDTH = 20;
  public static final int MAPHEIGHT = 12;

  public static final int TILE_EMPTY = 0;
  public static final int TILE_BRICKTRAP = 1;
  public static final int TILE_DIRTTRAP = 2;
  public static final int TILE_STEELTRAP = 3;
  public static final int TILE_HOLE = 4;
  public static final int TILE_LAST_FALLTHROUGH = 4;
  public static final int TILE_BRICK = 5;
  public static final int TILE_DIRT = 6;
  public static final int TILE_STEEL = 7;
  public static final int TILE_FIRST_CLIMBABLE = 8;
  public static final int TILE_LADDER = 8;
  public static final int TILE_ROPE = 9;
  public static final int TILE_CHAIN = 10;
  public static final int TILE_PHOLE = 11;
  public static final int TILE_CRYSTAL = 12;
  public static final int TILE_EXIT = 13;
  public static final int TILE_PLAYER = 14;
  public static final int TILE_MONSTER = 15;

  public static final int LEVEL_NORMAL = 0;
  public static final int LEVEL_EXITED = 1;
  public static final int LEVEL_RESET = 2;
  public static final int LEVEL_KILLED = 3;

  // Lock bits and masks
  private static final int LOCK_MASKS[] = {
        0x02, 0x01, 0x200000, 0x40000, 0x2000, 0x8000000, 0x10000, 0x80 };
  private static final int LOCK_BITS[] = {
        0x4000000, 0x20, 0x800000, 0x4000, 0x100, 0x200,0x20000000, 0x100000};
  private static final int LOCK_CKECKSUM = 0x40000040;
// *** VARIABLES ***
  // Maps that make up the set
  byte aSetMaps[][][] = new byte[SETLEVELS][MAPWIDTH][MAPHEIGHT];

  // Other set information
  String sCaption[] = new String[SETLEVELS];
  String sAuthor[] = new String[SETLEVELS];
  String sVersion[] = new String[SETLEVELS];
  boolean bLock[] = new boolean[SETLEVELS];

  // Actual map being played
  byte anMap[][];

  int nCurLevel;
  int nNumCrystals, nNumHoles;
  int nDelay;

  public Player player = new Player();
  public Vector vHoles = new Vector(8, 4);
  public Vector vCrystals = new Vector(16, 8);
  public Vector vMonsters = new Vector(4, 4);
  public LevelExit levelExit;

  boolean bFullRenderNeeded;

  boolean bSoundSet, bSoundEnabled;
  AudioClip auMonster;

// *** CONSTRUCTORS ***
  public Map() {
    anMap = new byte[MAPWIDTH][MAPHEIGHT];
    bFullRenderNeeded = true;
    nDelay = 1;
    bSoundSet = bSoundEnabled = false;
    auMonster = null;
    newSet();
  }

  public Map(String s) {
    loadSet(s);
    nDelay = 1;
    bFullRenderNeeded = true;
    bSoundSet = bSoundEnabled = false;
    auMonster = null;
  }

// *** METHODS - Set ***
  public void setSound(boolean b) {
    if (bSoundSet)
      bSoundEnabled = b;
    else
      bSoundEnabled = false;
  }

  public void setSoundClips(AudioClip auH, AudioClip auW, AudioClip auD, AudioClip auC, AudioClip auM) {
    player.setSoundClips(auH, auW, auD, auC);
    auMonster = auM;
    bSoundSet = true;
  }

  public void setTile(int x, int y, int tile) {
    anMap[x][y] = (byte)(tile);
  }

  public void setSetTile(int n, int x, int y, int tile) {
    aSetMaps[n][x][y] = (byte)(tile);
  }

  public void setFullRender(boolean b) {
    bFullRenderNeeded = b;
  }

  public void setDelay(int n) {
    nDelay = n;
  }

// *** METHODS - Get ***
  public boolean isFullRenderNeeded() {
    return bFullRenderNeeded;
  }

  public int getNumCrystals() {
    return nNumCrystals;
  }

  public int getNumHoles() {
    return nNumHoles;
  }

  public int getDelay() {
    return nDelay;
  }

  public final int getSetTile(int n, int x, int y) {
    return aSetMaps[n][x][y];
  }

  public final int getTile(int x, int y) {
    int r;
    try {
      r = anMap[x][y];
    }
    catch (ArrayIndexOutOfBoundsException e)
    {
      r = TILE_STEEL;
    }
    return r;
  }

  private final byte getMapTile(int x, int y) {
    byte r = aSetMaps[nCurLevel][x][y];

    if (r == 4)
      r = 5;
    else if (r > 10)
      r = 0;

    return r;
  }

  public void restoreTile(int x, int y) {
    anMap[x][y] = (byte)(getMapTile(x, y));
  }

// *** METHODS - File management ***
  public void newSet() {
    int cntrX, cntrY, cntrL;

    for (cntrL = 0; cntrL < 8; ++cntrL) {
      bLock[cntrL] = false;
      sCaption[cntrL] = "Untitled " + (cntrL+1);
      sAuthor[cntrL] = "Anonymous";
      sVersion[cntrL] = "0.00.01";
      for (cntrX = 0; cntrX < MAPWIDTH; ++cntrX)
        for (cntrY = 0; cntrY < MAPHEIGHT; ++cntrY)
          aSetMaps[cntrL][cntrX][cntrY] = 0;
    }
  }

  private int buildCheck() {
    Random r = new Random();
    int cntr, nCheck;

    for (cntr = 0; cntr < 10; ++cntr)
      nCheck = r.nextInt();
    nCheck = r.nextInt();

    nCheck |= LOCK_CKECKSUM;

    for (cntr = 0; cntr < 8; ++cntr) {
      if ((nCheck & LOCK_MASKS[cntr]) == 0)
        if (bLock[cntr])
          nCheck &= (~LOCK_BITS[cntr]);
        else
          nCheck |= LOCK_BITS[cntr];
      else
        if (bLock[cntr])
          nCheck |= LOCK_BITS[cntr];
        else
          nCheck &= (~LOCK_BITS[cntr]);
    }

    return nCheck;
  }

  private boolean restoreCheck(int nCheck) {
    if ((nCheck & LOCK_CKECKSUM) != LOCK_CKECKSUM)
      return false;

    boolean bMask, bBit;

    for (int cntr = 0; cntr < 8; ++cntr) {
      bMask = ((nCheck & LOCK_MASKS[cntr]) == LOCK_MASKS[cntr]);
      bBit = ((nCheck & LOCK_BITS[cntr]) == LOCK_BITS[cntr]);
      if (bMask)
        bLock[cntr] = bBit;
      else
        bLock[cntr] = !bBit;

//      System.out.println("Level " + cntr + " locked = " + bLock[cntr]);
    }
    return true;
  }

  /**
    Saves the set to the indicated file.
  */
  public void saveSet(String s) {
    int cntrX, cntrY, cntrL;

    try {
      // Open file
      DataOutputStream dos = new DataOutputStream(new FileOutputStream(s));

      // Save Map Checksum
      dos.writeInt(buildCheck());
      for (cntrL = 0; cntrL < 8; ++cntrL) {
        if (sCaption[cntrL] == null)
          sCaption[cntrL] = "Untitled " + cntrL;
        if (sAuthor[cntrL] == null)
          sAuthor[cntrL] = "Anonymous";
        if (sVersion[cntrL] == null)
          sVersion[cntrL] = "0.00.01";

        dos.writeUTF(sCaption[cntrL]);
        dos.writeUTF(sAuthor[cntrL]);
        dos.writeUTF(sVersion[cntrL]);
        for (cntrX = 0; cntrX < MAPWIDTH; ++cntrX)
          for (cntrY = 0; cntrY < MAPHEIGHT; ++cntrY)
            dos.writeByte(aSetMaps[cntrL][cntrX][cntrY]);
      }

      // Close file
      dos.close();
    }
		catch (IOException e) {
			System.out.println("IO Error");
		}
  }

  /**
    Loads the set from the indicated file.
  */
  public boolean loadSet(String s) {
    int cntrX, cntrY, cntrL;
    boolean bSuccess = true;

    try {
      // Open file
      DataInputStream dis = new DataInputStream(new FileInputStream(s));

      // Map Checksum
      if (restoreCheck(dis.readInt())) {
        for (cntrL = 0; cntrL < 8; ++cntrL) {
          sCaption[cntrL] = dis.readUTF();
          sAuthor[cntrL] = dis.readUTF();
          sVersion[cntrL] = dis.readUTF();
          for (cntrX = 0; cntrX < MAPWIDTH; ++cntrX)
            for (cntrY = 0; cntrY < MAPHEIGHT; ++cntrY)
              aSetMaps[cntrL][cntrX][cntrY] = dis.readByte();
        }
      }
      else
        bSuccess = false;
      // Close file
      dis.close();
    }
		catch (IOException e) {
			System.out.println("IO Error");
      bSuccess = false;
		}

    return bSuccess;
  }


  /**
    Loads the set from the indicated URL.
  */
  public boolean loadSet(URL u) {
    int cntrX, cntrY, cntrL;
    boolean bSuccess = true;

    try {
      // Open file
			DataInputStream dis = new DataInputStream(u.openStream());

      // Map Checksum
      if (restoreCheck(dis.readInt())) {
        for (cntrL = 0; cntrL < 8; ++cntrL) {
          sCaption[cntrL] = dis.readUTF();
          sAuthor[cntrL] = dis.readUTF();
          sVersion[cntrL] = dis.readUTF();
          for (cntrX = 0; cntrX < MAPWIDTH; ++cntrX)
            for (cntrY = 0; cntrY < MAPHEIGHT; ++cntrY)
              aSetMaps[cntrL][cntrX][cntrY] = dis.readByte();
        }
      }
      else
        bSuccess = false;
      // Close file
      dis.close();
    }
		catch (IOException e) {
			System.out.println("IO Error");
      bSuccess = false;
		}

    return bSuccess;
  }


// *** METHOD - ACTION ***
  public void startLevel(int nLvl) {
    // << CODE TO reset player's lives here >>
    restartLevel(nLvl);
  }

  public void nextLevel() {
    ++player.nLives;
    restartLevel(nCurLevel+1);
  }

  public void restartLevel(int nLvl) {
    int nTrueTile;

    player.restartLevel();

    nNumCrystals = 0;
    nNumHoles = 0;

    vHoles.removeAllElements();
    vCrystals.removeAllElements();
    vMonsters.removeAllElements();

    nCurLevel = nLvl;
    if (nCurLevel == 8)
      return;
    for (int cntrX = 0; cntrX < 20; ++cntrX)
      for (int cntrY = 0; cntrY < 12; ++cntrY) {
        anMap[cntrX][cntrY] = getMapTile(cntrX, cntrY);
        nTrueTile = aSetMaps[nLvl][cntrX][cntrY];
        if (nTrueTile > TILE_CHAIN) {
          switch (nTrueTile) {
            case TILE_PHOLE:
              ++nNumHoles;
              vCrystals.addElement(new PHole(cntrX*32, cntrY * 32));
              vHoles.addElement(new Hole(cntrX*32, cntrY * 32, false));
              break;
            case TILE_CRYSTAL:
              ++nNumCrystals;
              vCrystals.addElement(new Sprite(cntrX*32, cntrY * 32, true));
              break;
            case TILE_EXIT:
              levelExit = new LevelExit(cntrX*32, cntrY*32, false);
              break;
            case TILE_PLAYER:
              player.setX(cntrX * 32);
              player.setY(cntrY * 32);
              break;
            case TILE_MONSTER:
              vMonsters.addElement(new Monster(cntrX*32, cntrY*32));
              break;
            default:
              System.out.println("Error: INVALID TILE ON MAP!");
              break;
          }
        }
        else if (nTrueTile == TILE_HOLE) {
          vHoles.addElement(new Hole(cntrX*32, cntrY * 32, true));
          ++nNumHoles;
        }
      }
    bFullRenderNeeded = true;
  }

  public int doUpdate(int nTurns) {
    int cntrTurn, cntr, nIndex;
    int result = LEVEL_NORMAL;

    for (cntrTurn = 0; cntrTurn < nTurns; ++cntrTurn) {
      // Move the player
        synchronized(player)
        {
            player.doUpdate(this);
            if (player.hasExited())
                result = LEVEL_EXITED;
        }
        
      // Move the monsters
        synchronized(vMonsters)
        {
            for (cntr = 0, nIndex = 0; cntr < vMonsters.size(); ++cntr)
                if ( ((Monster)(vMonsters.elementAt(nIndex))).doUpdate(this) )
                    ++nIndex;
                else
                    vMonsters.removeElementAt(nIndex);
        }
        
      // Move the holes
        synchronized (vHoles)
        {
            for (cntr = 0; cntr < vHoles.size(); ++cntr)
                ((Hole)(vHoles.elementAt(cntr))).doUpdate(this);
        }
        
      // Move the crystals
        synchronized (vCrystals)
        {
            for (cntr = 0; cntr < vCrystals.size(); ++cntr)
                ((Sprite)(vCrystals.elementAt(cntr))).doUpdate(this);
        }
    }

    // Now check player's status
    int nPS = player.checkStatus(this);
    if (nPS != Player.RESULT_NORMAL)
      if (nPS == Player.RESULT_DIE)
        result = LEVEL_KILLED;
      else
        result = LEVEL_RESET;
        
    return result;
  }

  public void copyLevel(int nDest, Map m, int nSource) {
    int cntrX, cntrY;

    // Copy the map level
    for (cntrX = 0; cntrX < MAPWIDTH; ++cntrX) {
      for (cntrY = 0; cntrY < MAPHEIGHT; ++cntrY) {
        aSetMaps[nDest][cntrX][cntrY] = m.aSetMaps[nSource][cntrX][cntrY];
      }
    }

    // Copy Other set information
    sCaption[nDest] = m.sCaption[nSource];
    sAuthor[nDest] = m.sAuthor[nSource];
    sVersion[nDest] = m.sVersion[nSource];
    bLock[nDest] = m.bLock[nSource];
  }
}